using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using TomShane.Neoforce.Controls;

namespace BlackAndWhiteBalance
{
    public enum DrawOrders
    {
        PlayArea = 1000,
        GUI = 2000,
        Debug = 3000
    }

    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Core : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        public GraphicsDeviceManager Graphics
        {
            get { return graphics; }
        }
        SpriteBatch spriteBatch;

        Manager manager;
        public Manager GUIManager
        {
            get { return manager; }
        }

        HUD hud;

        static Dot[] dots;
        static uint[] colourData;

        Control[] controls;
        public Control[] Controls
        {
            get { return controls; }
        }

        uint numControls = 0;
        public uint NumControls
        {
            get { return numControls; }
            set
            {
                if (value > Const.MaxControls)
                    numControls = Const.MaxControls;
                else
                    numControls = value;
            }
        }

        bool controlsVisible = true;

        Texture2D playAreaTexture;

        bool isPaused = false;
        public bool IsPaused
        {
            get { return isPaused; }
            set { isPaused = value; }
        }

        float timeScale = 1f;
        public float TimeScale
        {
            get { return timeScale; }
            set { timeScale = value; }
        }

        int currentControl = 0;
        public int CurrentControl
        {
            get { return currentControl; }
            set 
            {
                currentControl = value;
                hud.SetupCurrentControl(currentControl);
            }
        }

        bool isCurrentControlGrabbed = false;
        public bool IsCurrentControlGrabbed
        {
            get { return isCurrentControlGrabbed; }
            set { isCurrentControlGrabbed = value; }
        }

        ResolveTexture2D originalScreenResolveTexture;

        bool blurEnabled = false;
        public bool BlurEnabled
        {
            get { return blurEnabled; }
            set { blurEnabled = value; }
        }

        Effect blurEffect;
        float blurAmount = 0.5f;
        public float BlurAmount
        {
            get { return blurAmount; }
            set { blurAmount = value; }
        }

        bool blackAndWhiteEnabled = false;
        public bool BlackAndWhiteEnabled
        {
            get { return blackAndWhiteEnabled; }
            set { blackAndWhiteEnabled = value; }
        }

        Effect blackAndWhiteEffect;
        float blackAndWhiteThreshold = 0.5f;
        public float BlackAndWhiteThreshold
        {
            get { return blackAndWhiteThreshold; }
            set { blackAndWhiteThreshold = value; }
        }

        public Core()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            this.graphics.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>(graphics_PreparingDeviceSettings);

            this.IsFixedTimeStep = false;
            this.graphics.SynchronizeWithVerticalRetrace = false;
            this.IsMouseVisible = true;
            this.graphics.IsFullScreen = false;
            this.graphics.PreferredBackBufferWidth = Const.WindowWidth;
            this.graphics.PreferredBackBufferHeight = Const.WindowHeight;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            dots = new Dot[DotSystem.MaxDots];
            for (int i = 0; i < DotSystem.MaxDots; i++)
            {
                dots[i] = new Dot();
            }

            RandomizeDots();

            manager = new Manager(this, this.Graphics);
            manager.Initialize();

            Control.AttractiveIconEnabled = Content.Load<Texture2D>(@"ControlSprites\Attractive_Enabled");
            Control.RepulsiveIconEnabled = Content.Load<Texture2D>(@"ControlSprites\Repulsive_Enabled");
            Control.PushUpIconEnabled = Content.Load<Texture2D>(@"ControlSprites\PushUp_Enabled");
            Control.PushDownIconEnabled = Content.Load<Texture2D>(@"ControlSprites\PushDown_Enabled");

            Control.AttractiveIconDisabled = Content.Load<Texture2D>(@"ControlSprites\Attractive_Disabled");
            Control.RepulsiveIconDisabled = Content.Load<Texture2D>(@"ControlSprites\Repulsive_Disabled");
            Control.PushUpIconDisabled = Content.Load<Texture2D>(@"ControlSprites\PushUp_Disabled");
            Control.PushDownIconDisabled = Content.Load<Texture2D>(@"ControlSprites\PushDown_Disabled");

            Control.AttractiveIconEnabledSelected = Content.Load<Texture2D>(@"ControlSprites\Attractive_Enabled_Selected");
            Control.RepulsiveIconEnabledSelected = Content.Load<Texture2D>(@"ControlSprites\Repulsive_Enabled_Selected");
            Control.PushUpIconEnabledSelected = Content.Load<Texture2D>(@"ControlSprites\PushUp_Enabled_Selected");
            Control.PushDownIconEnabledSelected = Content.Load<Texture2D>(@"ControlSprites\PushDown_Enabled_Selected");

            Control.AttractiveIconDisabledSelected = Content.Load<Texture2D>(@"ControlSprites\Attractive_Disabled_Selected");
            Control.RepulsiveIconDisabledSelected = Content.Load<Texture2D>(@"ControlSprites\Repulsive_Disabled_Selected");
            Control.PushUpIconDisabledSelected = Content.Load<Texture2D>(@"ControlSprites\PushUp_Disabled_Selected");
            Control.PushDownIconDisabledSelected = Content.Load<Texture2D>(@"ControlSprites\PushDown_Disabled_Selected");

            Control.InvisbleIcon = Content.Load<Texture2D>(@"ControlSprites\Invisible");

            controls = new Control[Const.MaxControls];
            for (int i = 0; i < controls.Length; i++)
            {
                controls[i] = new Control(this, manager, i);
            }
            controls[0].Visible = true;

            AddControl(); 
            isCurrentControlGrabbed = true;

            FPS fps = new FPS(this);
            fps.DrawOrder = (int)DrawOrders.Debug;
            Components.Add(fps);

            base.Initialize();
        }

        public void RandomizeDots()
        {
            for (int i = 0; i < DotSystem.MaxDots; i++)
            {
                dots[i].position = new Vector2(RandomHelper.rand.Next(0, Const.PlayAreaDotWidth), RandomHelper.rand.Next(0, Const.PlayAreaDotHeight));
            }
        }

        void graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            e.GraphicsDeviceInformation.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PreserveContents;
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            colourData = new uint[Const.PlayAreaDotWidth * Const.PlayAreaDotHeight];
            playAreaTexture = new Texture2D(GraphicsDevice, Const.PlayAreaDotWidth, Const.PlayAreaDotHeight);

            Components.Add(hud = new HUD(this));

            // Look up the resolution and format of our main backbuffer.
            PresentationParameters pp = GraphicsDevice.PresentationParameters;
            SurfaceFormat format = pp.BackBufferFormat;
            originalScreenResolveTexture = new ResolveTexture2D(GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, 1, format);

            blackAndWhiteEffect = Content.Load<Effect>(@"Effects\BlackAndWhite");
            blurEffect = Content.Load<Effect>(@"Effects\GaussianBlur");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            float time = gameTime.ElapsedGameTime.Milliseconds * timeScale;

            // Update user movement of current control:
            if (isCurrentControlGrabbed)
            {
                MouseState ms = Mouse.GetState();
                Vector2 newPosition = new Vector2((ms.X - Const.PlayAreaRenderRectangle.Left) / Const.PlayAreaToScreenScale, (ms.Y - Const.PlayAreaRenderRectangle.Top) / Const.PlayAreaToScreenScale);
                
                // Check new position to make sure it's within bounds:
                if (newPosition.X < 0)
                    newPosition.X = 0;
                else if (newPosition.X > Const.PlayAreaDotWidth - 1)
                    newPosition.X = Const.PlayAreaDotWidth - 1;

                if (newPosition.Y < 0)
                    newPosition.Y = 0;
                else if (newPosition.Y > Const.PlayAreaDotHeight - 1)
                    newPosition.Y = Const.PlayAreaDotHeight - 1;

                controls[CurrentControl].Position = newPosition;
            }

            if (!isPaused)
            {
                // Clear previous colour data:
                for (int i = 0; i < Const.PlayAreaDotWidth * Const.PlayAreaDotHeight; i++)
                {
                    colourData[i] = Const.BGColour;
                }

                // update velocity and position of dots and set their colour data:
                for (int i = 0; i < DotSystem.numDots; i++)
                {
                    Vector2 velocity = Vector2.Zero;
                    for (int c = 0; c < numControls; c++)
                    {
                        if (controls[c].Enabled)
                        {
                            // Figure out where to accellerate towards:
                            Vector2 vectorToControl = (dots[i].position - controls[c].Position);

                            if (!(vectorToControl.X == 0f && vectorToControl.Y == 0f))
                            {
                                switch (controls[c].Type)
                                {
                                    case Control.ControlType.Attractive:
                                        velocity += -vectorToControl / vectorToControl.LengthSquared() * controls[c].strength;
                                        break;
                                    case Control.ControlType.Repulsive:
                                        velocity += vectorToControl / vectorToControl.LengthSquared() * controls[c].strength;
                                        break;
                                    case Control.ControlType.PushUp:
                                        velocity += -Vector2.One * controls[c].strength * 10f / vectorToControl.LengthSquared();
                                        break;
                                    case Control.ControlType.PushDown:
                                        velocity += Vector2.One * controls[c].strength * 10f / vectorToControl.LengthSquared();
                                        break;
                                }
                            }
                        }
                    }

                    dots[i].velocity = velocity;

                    // update position based on the velocity (and time per frame)
                    dots[i].position += velocity * time;

                    // Check new position to make sure it's within bounds:
                    if (dots[i].position.X < 0)
                        dots[i].position.X = 0;
                    else if (dots[i].position.X > Const.PlayAreaDotWidth - 1)
                        dots[i].position.X = Const.PlayAreaDotWidth - 1;
                    else if (float.IsNaN(dots[i].position.X) || float.IsInfinity(dots[i].position.X))
                    {
#if DEBUG
                        throw new InvalidProgramException("Dot is pretty messed up!");
#endif
                        dots[i].position.X = 0;
                        dots[i].velocity = Vector2.Zero;
                    }

                    if (dots[i].position.Y < 0)
                        dots[i].position.Y = 0;
                    else if (dots[i].position.Y > Const.PlayAreaDotHeight - 1)
                        dots[i].position.Y = Const.PlayAreaDotHeight - 1;
                    else if (float.IsNaN(dots[i].position.Y) || float.IsInfinity(dots[i].position.Y))
                    {
#if DEBUG
                        throw new InvalidProgramException("Dot is pretty messed up!");
#endif
                        dots[i].position.Y = 0;
                        dots[i].velocity = Vector2.Zero;
                    }

                    // Create colour data based on position of this dot:
                    colourData[(int)dots[i].position.X + ((int)dots[i].position.Y * Const.PlayAreaDotWidth)] = Const.DotColour;
                }

                playAreaTexture.SetData<uint>(colourData);

            }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.White);
            ResetGraphicsDevice();

            spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
            spriteBatch.Draw(playAreaTexture, Const.PlayAreaRenderRectangle, null, Color.White);
            spriteBatch.End();

            if (blurEnabled || blackAndWhiteEnabled)
            {
                GraphicsDevice.ResolveBackBuffer(originalScreenResolveTexture);
            }

            // Blur:
            if (blurEnabled)
            {
                blurEffect.CurrentTechnique = blurEffect.Techniques["GaussianBlur"];
                // horizontal gaussian blur filter:
                SetBlurEffectParameters(1.0f / (float)originalScreenResolveTexture.Width, 0);

                spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
                blurEffect.Begin();
                blurEffect.CurrentTechnique.Passes[0].Begin();

                spriteBatch.Draw(originalScreenResolveTexture, Vector2.Zero, Color.White);

                blurEffect.CurrentTechnique.Passes[0].End();
                blurEffect.End();
                spriteBatch.End();

                GraphicsDevice.ResolveBackBuffer(originalScreenResolveTexture);

                // vertical gaussian blur filter:
                SetBlurEffectParameters(0, 1.0f / (float)originalScreenResolveTexture.Height);

                spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
                blurEffect.Begin();
                blurEffect.CurrentTechnique.Passes[0].Begin();

                spriteBatch.Draw(originalScreenResolveTexture, Vector2.Zero, Color.White);

                blurEffect.CurrentTechnique.Passes[0].End();
                blurEffect.End();
                spriteBatch.End();

                GraphicsDevice.ResolveBackBuffer(originalScreenResolveTexture);
            }

            // Black and White Mode:
            if(blackAndWhiteEnabled)
            {
                spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
                
                blackAndWhiteEffect.CurrentTechnique = blackAndWhiteEffect.Techniques["BlackAndWhite"];
                blackAndWhiteEffect.Parameters["Threshold"].SetValue(blackAndWhiteThreshold);
                blackAndWhiteEffect.Begin();
                blackAndWhiteEffect.CurrentTechnique.Passes[0].Begin();

                spriteBatch.Draw(originalScreenResolveTexture, Vector2.Zero, Color.White);

                blackAndWhiteEffect.CurrentTechnique.Passes[0].End();
                blackAndWhiteEffect.End();
                spriteBatch.End();
            }

            base.Draw(gameTime);
        }
        
        /// <summary>
        /// Computes sample weightings and texture coordinate offsets
        /// for one pass of a separable gaussian blur filter.
        /// </summary>
        void SetBlurEffectParameters(float dx, float dy)
        {
            // Look up the sample weight and offset effect parameters.
            EffectParameter weightsParameter, offsetsParameter;

            weightsParameter = blurEffect.Parameters["SampleWeights"];
            offsetsParameter = blurEffect.Parameters["SampleOffsets"];

            // Look up how many samples our gaussian blur effect supports.
            int sampleCount = weightsParameter.Elements.Count;

            // Create temporary arrays for computing our filter settings.
            float[] sampleWeights = new float[sampleCount];
            Vector2[] sampleOffsets = new Vector2[sampleCount];

            // The first sample always has a zero offset.
            sampleWeights[0] = ComputeGaussian(0);
            sampleOffsets[0] = new Vector2(0);

            // Maintain a sum of all the weighting values.
            float totalWeights = sampleWeights[0];

            // Add pairs of additional sample taps, positioned
            // along a line in both directions from the center.
            for (int i = 0; i < sampleCount / 2; i++)
            {
                // Store weights for the positive and negative taps.
                float weight = ComputeGaussian(i + 1);

                sampleWeights[i * 2 + 1] = weight;
                sampleWeights[i * 2 + 2] = weight;

                totalWeights += weight * 2;

                // To get the maximum amount of blurring from a limited number of
                // pixel shader samples, we take advantage of the bilinear filtering
                // hardware inside the texture fetch unit. If we position our texture
                // coordinates exactly halfway between two texels, the filtering unit
                // will average them for us, giving two samples for the price of one.
                // This allows us to step in units of two texels per sample, rather
                // than just one at a time. The 1.5 offset kicks things off by
                // positioning us nicely in between two texels.
                float sampleOffset = i * 2 + 1.5f;

                Vector2 delta = new Vector2(dx, dy) * sampleOffset;

                // Store texture coordinate offsets for the positive and negative taps.
                sampleOffsets[i * 2 + 1] = delta;
                sampleOffsets[i * 2 + 2] = -delta;
            }

            // Normalize the list of sample weightings, so they will always sum to one.
            for (int i = 0; i < sampleWeights.Length; i++)
            {
                sampleWeights[i] /= totalWeights;
            }

            // Tell the effect about our new filter settings.
            weightsParameter.SetValue(sampleWeights);
            offsetsParameter.SetValue(sampleOffsets);
        }


        /// <summary>
        /// Evaluates a single point on the gaussian falloff curve.
        /// Used for setting up the blur filter weightings.
        /// </summary>
        float ComputeGaussian(float n)
        {
            float theta = blurAmount;

            return (float)((1.0 / Math.Sqrt(2 * Math.PI * theta)) *
                           Math.Exp(-(n * n) / (2 * theta * theta)));
        }

        public void ResetGraphicsDevice()
        {
            // Set up graphics device defaults (if you change them latter on, please change back!
            GraphicsDevice.RenderState.DepthBufferEnable = false;
            GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

            // Make sure alpha blending is on:
            GraphicsDevice.RenderState.AlphaBlendEnable = true;
            GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
            GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
        }

        public void AddControl()
        {
            uint oldNumControls = numControls;
            // try adding it, see if it caps:
            NumControls++;
            if (oldNumControls < numControls)
            {
                controls[numControls - 1].Visible = controlsVisible;
                controls[numControls - 1].Position = new Vector2(Const.PlayAreaDotWidth / 2, Const.PlayAreaDotHeight / 2);

                SelectControl((int)numControls - 1);
                GrabControl((int)numControls - 1);
            }
        }

        public void RemoveControl(int id)
        {
            // put the old control at the end of the array and shift all controls with higher indexes back one to overwrite it
            Control oldControl = controls[id];
            for (int i = id; i < Const.MaxControls - 1; i++)
            {
                controls[i] = controls[i + 1];
                controls[i].Id = i;
            }
            controls[Const.MaxControls - 1] = oldControl;

            // set up old control to be as if it never had existed:
            controls[Const.MaxControls - 1].ResetToNonExistant((int)Const.MaxControls - 1);

            NumControls--;
            SelectControl(0);
        }

        internal void ClickControl(int id)
        {
            controls[currentControl].IsSelected = false;

            if (currentControl != id)
            {
                CurrentControl = id;
                isCurrentControlGrabbed = false;
            }
            else if (!isCurrentControlGrabbed)
            {
                GrabControl(id);
            }
            else
            {
                isCurrentControlGrabbed = false;
            }

            controls[currentControl].IsSelected = true;
        }

        public void SelectControl(int id)
        {
            controls[currentControl].IsSelected = false;
            if (currentControl != id)
            {
                CurrentControl = id;
                isCurrentControlGrabbed = false;
            }
            controls[currentControl].IsSelected = true;
        }

        public void GrabControl(int id)
        {
            Vector2 controlPositionWindowCoords = ConvertPlayAreaCoordsToWindowCoords(controls[id].Position);
            Mouse.SetPosition((int)controlPositionWindowCoords.X, (int)controlPositionWindowCoords.Y);
            isCurrentControlGrabbed = true;
        }

        public void GrabNextControlInList()
        {
            if (numControls > 0)
            {
                int nextControlID = currentControl + 1;
                if (nextControlID > numControls - 1)
                    nextControlID = 0;
                SelectControl(nextControlID);
                GrabControl(nextControlID);
            }
        }

        internal void ToggleControlVisibility()
        {
            bool visible = !controlsVisible;
            for (int i = 0; i < numControls; i++)
            {
                controls[i].Visible = visible;
            }
            controlsVisible = visible;
        }

        public Vector2 ConvertPlayAreaCoordsToWindowCoords(Vector2 position)
        {
            return new Vector2(((int)position.X * Const.PlayAreaToScreenScale) + Const.PlayAreaRenderRectangle.Left,
                ((int)position.Y * Const.PlayAreaToScreenScale) + Const.PlayAreaRenderRectangle.Top);
        }
    }
}
